﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace V32VM
{
    class Memory : IDisposable
    {
        private class Page
        {
            string path;
            public readonly byte[] memory;

            // loads a page with given bytes in memory
            public Page(byte[] memory, uint size)
            {
                if (memory.Length != size)
                    throw new ArgumentException("memory");
                this.memory = memory;
            }
            // loads a page from the given binary file
            public Page(string file, uint size)
            {
                path = file;
                try
                {
                    memory = File.ReadAllBytes(file);
                    if (memory.Length != size)
                    {
                        memory = null;
                        throw new ArgumentException("file");
                    }
                }
                catch (IOException)
                {
                    memory = new byte[size];
                }
            }

            // saves the page to a file.
            // if path is null, uses the path the page was loaded from.
            public void save(string path = null)
            {
                if (path == null)
                    path = this.path;
                new FileInfo(path).Directory.Create();
                File.WriteAllBytes(path, memory);
            }
        }


        string pagePath;
        uint maxPages;
        uint pageSize;
        Dictionary<uint, Page> pages;
        Queue<uint> pageUse;

        uint virtualMemStart,
            virtualMemLen;

        // the token [page] in pagePath will be replaced with a
        // given page index to find the page's binary dump
        public Memory(string pagePath, uint pageSize, uint maxPages)
        {
            this.pagePath = pagePath;
            this.pageSize = pageSize;
            this.maxPages = maxPages;
            this.pages = new Dictionary<uint, Page>();
            this.pageUse = new Queue<uint>((int)this.maxPages);
            this.words = new MemoryWords(this);

            this.virtualMemStart = 0;
            this.virtualMemLen = 0;
        }

        public void setVirtualSpace(uint start, uint len)
        {
            virtualMemStart = start;
            virtualMemLen = len;
        }

        // gets the page for a certain address,
        // and updates page usage
        Page getPage(uint ip, uint address, bool forceAbsolute = false)
        {
            // we are only in virtual memory if the instruction pointer (ip) is in virtual memory
            // and we are not forced to use physical addressing
            if (!forceAbsolute && ip >= virtualMemStart && ip < virtualMemStart + virtualMemLen)
            {
                // virtual memory; ensure valid address and
                // then offset virtual address to get physical address
                if (address < 0 || address >= virtualMemStart)
                    throw new Exception("Address pouints to nonexistent memory.");
                address += virtualMemStart;
            }
            uint index = address / pageSize;

            Page page;
            if (!pages.TryGetValue(index, out page))
                page = load(index);

            if (pageUse.Count == maxPages)
                pageUse.Dequeue();
            pageUse.Enqueue(index);

            return page;
        }

        // loads a page uinto memory, and discards 
        // another if there is no slot available
        Page load(uint page)
        {
            if (pages.Count == maxPages)
                discardPage();
            return pages[page] = new Page(pagePath.Replace("[page]", page.ToString()), pageSize);
        }

        // discards the latest-used page currently in memory
        void discardPage()
        {
            bool foundDiscardable = false;
            uint lastUsed = 0;
            foreach (uint used in pageUse)
            {
                if (pages.ContainsKey(used))
                {
                    foundDiscardable = true;
                    lastUsed = used;
                    break;
                }
            }
            if (!foundDiscardable)
                throw new Exception("I'm pretty sure this should never happen...");
            Page page = pages[lastUsed];
            page.save();
            pages.Remove(lastUsed);
        }

        // get/set a single byte
        public byte getByte(uint ip, uint address)
        {
            Page page = getPage(ip, address);
            return page.memory[address % pageSize];
        }
        public void setByte(uint ip, uint address, byte value)
        {
            getPage(ip, address).memory[address % pageSize] = value;
        }

        // get/set a uint
        public uint getUint(uint ip, uint address)
        {
            return (uint)getByte(ip, address + 0) << 0
                 | (uint)getByte(ip, address + 1) << 8
                 | (uint)getByte(ip, address + 2) << 16
                 | (uint)getByte(ip, address + 3) << 24;
        }
        public void setUint(uint ip, uint address, uint value)
        {
            setByte(ip, address + 0, (byte)(value >> 0));
            setByte(ip, address + 1, (byte)(value >> 8));
            setByte(ip, address + 2, (byte)(value >> 16));
            setByte(ip, address + 3, (byte)(value >> 24));
        }

        // indexers
        public byte this[uint ip, uint address]
        {
            get { return getByte(ip, address); }
            set { setByte(ip, address, value); }
        }
        public MemoryWords words { get; private set; }

        // startAddress is a physical address (never virtual)
        public void loadBytes(byte[] bytes, uint startAddress)
        {
            for (uint i = 0; i < bytes.Length; i++)
                setByteReal(startAddress + i, bytes[i]);
        }
        // no of thise "virtual" memory stuff. we only use cold, 
        // hard, physical addresses in this part of town.
        void setByteReal(uint address, byte value)
        {
            getPage(0, address, true).memory[address % pageSize] = value;
        }

        // saves all pages still in memory
        public void Dispose()
        {
            foreach (Page page in pages.Values)
                page.save();
            pages = null;
        }
    }
    class MemoryWords
    {
        Memory memory;

        public MemoryWords(Memory memory)
        {
            this.memory = memory;
        }

        public uint this[uint ip, uint address]
        {
            get { return memory.getUint(ip, address); }
            set { memory.setUint(ip, address, value); }
        }
    }
}
